----------------Oo-Topos---------------
A 4am crack                  2018-01-26
-------------------. updated 2018-01-29
                   |___________________

Name: Oo-Topos
Genre: adventure
Year: 1986
Credits: story by Michael and Muffy
  Berlyn, graphics by Raimund Redlich
  and Brian Poff
Publisher: Polarware
Platform: Apple ][+ or later
  (double hi-res graphics require 128K)
Media: 5.25-inch disk
Sides: 2
OS: ProDOS

This disk was automatically cracked by
Passport. Here is the transcript for
side 2 (the bootable side):

                 --v--

READING FROM S6,D1
T00,S00 FOUND PRODOS BOOTLOADER
T00,S0B VOLUME NAME IS RUN
USING BUILT-IN RWTS
WRITING TO S5,D2
T0E,S05 FOUND FBFF PROTECTION CHECK
T0E,S05,$35: FB -> A2
T0E,S0D FOUND FBFF PROTECTION CHECK
T0E,S0D,$95: EB -> B2
CRACK COMPLETE. PRESS ANY KEY

                 --^--

More information and source code is
available at
https://archive.org/details/Passport4am

Side 1 is unprotected.

Quod erat liberand one more thing...

                   ~

This game has an obvious protection
check -- obvious in the sense that the
game freezes after selecting standard
or double hi-res graphics. This code is
on T0E,S05, encrypted with a one-byte
XOR #$FF. It is identical to the code
in "The Electric Crayon: ABCs" (see 4am
crack no. 622), and the patch is the
same: change the "BNE +4" near the
beginning of the encrypted code to "BNE
+5D" so the routine returns before the
Death Counter hits 0.

Here's that code in Disk Fixer sector
editor:

                 --v--

T0E,S05
----------- DISASSEMBLY MODE ----------
; save some state from zero page
0000:A2 03          LDX   #$03
0002:B5 00          LDA   $00,X
0004:48             PHA
0005:CA             DEX
0006:10 FA          BPL   $0002

; put an "RTS" at $00 and call it (WTF)
0008:A9 60          LDA   #$60
000A:85 00          STA   $00
000C:20 00 00       JSR   $0000

; do some stack pointer manipulation
; (more on this in a minute)
000F:BA             TSX
0010:CA             DEX
0011:CA             DEX
0012:9A             TXS
0013:68             PLA
0014:85 00          STA   $00
0016:68             PLA
0017:85 01          STA   $01

; decrypt the code that follows
0019:A0 89          LDY   #$89
001B:A9 FF          LDA   #$FF
001D:51 00          EOR   ($00),Y
001F:91 00          STA   ($00),Y
0021:88             DEY
0022:C0 17          CPY   #$17
0024:D0 F5          BNE   $001B

                 --v--

Here's why we're putting an "RTS" at
$00 and called it. This entire routine
is relocatable. There's no "program
counter" on the Apple II. I mean,
obviously there is a program counter,
but it's not like a simple address that
you can read to see what instruction is
going to be executed next.

But this routine figures out where in
memory it lives, by creating a routine
that does nothing, calling it, and
taking advantage of the fact that the
"stack" is just a page of memory (from
$0100..$01FF) and a register that is an
index into that page. After the call to
$0000 (at offset $000C), the return
address is still somewhere in the
$0100..$01FF range. The stack page is
not overwritten until you call a
different subroutine or do something
else that pushes bytes to the stack.

This routine figures out where the
return address is in the stack page and
stores it in $00/$01. Then the loop at
offset $0019 decrypts memory backwards
from that address + $89, back to that
address + $17, then stops. $17 bytes is
the size of the code that manipulates
the stack pointer, plus the decryption
loop itself.

Anyway, the code immediately after the
decryption loop is encrypted, but we
can still view it in Disk Fixer. Press
"X" to set an EOR mask of $FF (leave
the ADD, AND, and OR masks alone), then
press <Ctrl-T> to apply the mask. Now
we can see the decrypted code starting
at offset $0026.

                 --v--

----------- DISASSEMBLY MODE ----------
; get slot number (x16) from ProDOS
0026:AE 30 BF       LDX   $BF30

; turning on the disk motor manually:
; never not suspicious
0029:BD 89 C0       LDA   $C089,X

; initialize Death Counters, probably
002C:A9 56          LDA   #$56
002E:85 03          STA   $03
0030:A9 08          LDA   #$08
0032:C6 02          DEC   $02
0034:D0 04          BNE   $003A

; if Death Counter hits zero, branch
0036:C6 03          DEC   $03
0038:F0 59          BEQ   $0093

; look for an $FB nibble
003A:BC 8C C0       LDY   $C08C,X
003D:10 FB          BPL   $003A
003F:C0 FB          CPY   #$FB
0041:D0 ED          BNE   $0030
0043:F0 00          BEQ   $0045

; kill a few cycles (not pointless,
; because the disk spins independently
; of the CPU, so all of these low-level
; disk reads are highly time-sensitive)
0045:EA             NOP
0046:EA             NOP

; read data latch (note: no BPL loop
; here, we're just reading it once)
0047:BC 8C C0       LDY   $C08C,X

; do a compare to set or clear the
; carry bit (among other things, but
; it's the carry bit we care about)
004A:C0 08          CPY   #$08

; rotate the carry into the accumulator
004C:2A             ROL

; if we just rolled a "1" bit out of
; the accumulator, take this branch
004D:B0 0B          BCS   $005A

; next nibble needs to be $FF
004F:BC 8C C0       LDY   $C08C,X
0052:10 FB          BPL   $004F

; ...otherwise we start over
0054:C0 FF          CPY   #$FF
0056:D0 D8          BNE   $0030

; loop back to get next nibble
; (unconditional branch since we didn't
; take the BNE just before it)
0058:F0 EB          BEQ   $0045

; execution continues here (from offset
; $004D) -- if the accumulator is
; anything but %00001010, start over
005A:C9 0A          CMP   #$0A
005C:D0 D2          BNE   $0030

; next nibbles must be "D5 AA 96 AA AB"
005E:BD 8C C0       LDA   $C08C,X
0061:10 FB          BPL   $005E
0063:C9 D5          CMP   #$D5
0065:D0 C9          BNE   $0030
0067:F0 00          BEQ   $0069
0069:BD 8C C0       LDA   $C08C,X
006C:10 FB          BPL   $0069
006E:C9 AA          CMP   #$AA
0070:D0 BE          BNE   $0030
0072:F0 00          BEQ   $0074
0074:BD 8C C0       LDA   $C08C,X
0077:10 FB          BPL   $0074
0079:C9 96          CMP   #$96
007B:D0 B3          BNE   $0030
007D:F0 00          BEQ   $007F
007F:BD 8C C0       LDA   $C08C,X
0082:10 FB          BPL   $007F
0084:C9 AA          CMP   #$AA
0086:D0 A8          BNE   $0030
0088:F0 00          BEQ   $008A
008A:BD 8C C0       LDA   $C08C,X
008D:10 FB          BPL   $008A
008F:C9 AB          CMP   #$AB
0091:D0 9D          BNE   $0030

I got lost several times trying to
follow this routine. I think the
easiest way to explain it is to show
the difference between the original
disk and my non-working copy.

Here is the original disk, as seen by
the Copy II+ nibble editor. Nibbles
with extra "0" bits (timing bits) after
them are displayed in inverse on an
original machine, marked here with a
"+" after the nibble.

                 --v--

   COPY ][ PLUS BIT COPY PROGRAM 8.4
(C) 1982-9 CENTRAL POINT SOFTWARE, INC.
---------------------------------------

TRACK:     START: 1B1E  LENGTH: 17C1

1C70: 9F EB E5 FC D7 D7 D7 EE   VIEW
1C78: FA E6 E6 FF FE F2 ED FD
1C80: FF EF ED BA BB DD AF E6
1C88: B7 A7 CB B7 DE AA EB FF
1C90: FF FF FF FB+FF FF+FF FF+
1C98: FD FF+FF+FF+FF+FF+FF+FF+
1CA0: FF+FF+D5 AA 96 AA AB AA
1CA8: AA AA AB AA AA AB FF FF+
1CB0: FF+FF+FF+FF+FF+FF D5 AA
---------------------------------------

  A  TO ANALYZE DATA  ESC TO QUIT

  ?  FOR HELP SCREEN  /  CHANGE PARMS

  Q  FOR NEXT TRACK   SPACE TO RE-READ

                 --^--

It's easy to understand why a simple
sector copy failed. The sequence that
this code is looking for starts at
offset $1C93, which is between the end
of one sector and the beginning of the
next. (The data epilogue is at $1C8C;
the next address prologue is at $1CA2.)
Sector copiers discard everything
between those delimiters and rebuild
the track with a default pattern of
sync bytes. That pattern doesn't
include an $FB nibble, so the nibble
check fails.

But the EDD bit copy also failed. Here
is the original disk's pattern at
offset $1C93:

  - $FB + timing bit
  - $FF
  - $FF + timing bit
  - $FF
  - $FF + timing bit

And here is what the same part of the
track looks like on my failed EDD copy:

                 --v--

   COPY ][ PLUS BIT COPY PROGRAM 8.4
(C) 1982-9 CENTRAL POINT SOFTWARE, INC.
---------------------------------------

TRACK:     START: 1B1E  LENGTH: 17C1

1C70: 9F EB E5 FC D7 D7 D7 EE   VIEW
1C78: FA E6 E6 FF FE F2 ED FD
1C80: FF EF ED BA BB DD AF E6
1C88: B7 A7 CB B7 DE AA EB FF
1C90: FF FF FF FB+FF FF FF+FF+
1C98: FD FF+FF+FF+FF+FF+FF+FF+
1CA0: FF+FF+D5 AA 96 AA AB AA
1CA8: AA AA AB AA AA AB FF FF+
1CB0: FF+FF+FF+FF+FF+FF D5 AA
---------------------------------------

  A  TO ANALYZE DATA  ESC TO QUIT

  ?  FOR HELP SCREEN  /  CHANGE PARMS

  Q  FOR NEXT TRACK   SPACE TO RE-READ

                 --^--

A subtle difference! The sequence at
offset $1C93 now looks like this:

  - $FB + timing bit
  - $FF
  - $FF
  - $FF + timing bit
  - $FF + timing bit

This code is looking for $FF bytes with
an alternating pattern of timing bit,
no timing bit, timing bit, no timing
bit. The accumulator holds the pattern
of whether each sync byte had a timing
bit after it. It's set one bit at a
time, rotated into place from the carry
bit that was set by the "CPY #$08" that
happened after getting the value of the
data latch (LDY $C08C,X) that happened
after doing just enough NOPs that the
value of the data latch will depend on
the presence of a timing bit after the
previous nibble.

Which is brilliant.

Anyway, if the value of the accumulator
(i.e. the pattern of timing bits) is
wrong, the program knows it's not
running on an original disk.

                   ~

Continuing from offset $0093...

; turn off drive motor
; (hey, this was also the failure path,
; which means the real success/failure
; check is later)
0093:DD 88 C0       CMP   $C088,X
0096:A0 89          LDY   #$89

And that's the end of the encrypted
portion. Now press <Ctrl-T> again to
disable the mask, and read the rest of
the code.

                 --v--

----------- DISASSEMBLY MODE ----------
; re-encrypt the protection check
0098:A9 FF          LDA   #$FF
009A:51 00          EOR   ($00),Y
009C:91 00          STA   ($00),Y
009E:88             DEY
009F:C0 17          CPY   #$17
00A1:D0 F5          BNE   $0098

; check the Death Counter
00A3:A4 03          LDY   $03

; restore zero page state
00A5:A2 00          LDX   #$00
00A7:68             PLA
00A8:95 00          STA   $00,X
00AA:E8             INX
00AB:E0 04          CPX   #$04
00AD:D0 F8          BNE   $00A7

; ah! if the nibble check failed, zero
; page $03 will be 0, which means this
; will fall into an infinite loop
00AF:98             TYA
00B0:F0 FE          BEQ   $00B0

; WARNING: side effects detected!
00B2:A9 00          LDA   #$00
00B4:8D D0 9A       STA   $9AD0
00B7:A9 C6          LDA   #$C6
00B9:8D D1 9A       STA   $9AD1
00BC:60             RTS

                 --^--

If the protection check fails, we get
into an infinite loop (at offset $00B0)
and this routine never returns. That
explains the behavior I saw on my non-
working copy.

But if the protection check succeeds,
we set two seemingly arbitrary memory
locations. Experience has taught me
that side effects of protection checks
are never innocuous, so I'll need to
be sure that my crack sets them too
(while bypassing all the protect-y bits
of course).

Backing up to the beginning of the
protection code, I see this branch, on
the low byte of the 16-bit Death
Counter:

0032:C6 02          DEC   $02
0034:D0 04          BNE   $003A

If I changed that branch to go straight
to the exit path (at offset $0093), the
high byte (zp$03) would still be non-
zero, and the protection check would
succeed immediately. Then I can piggy-
back on the existing post-check side
effects instead of having to recreate
them.

One problem: this code is encrypted on
disk. But Disk Fixer comes to the
rescue again, because we can re-apply
the EOR mask, change the branch value
we want at offset $0035 (from #$04 to
#$5D, so it branches to offset $0093),
then disable the EOR mask one final
time and save the sector to disk.

That's the first patch that Passport
applied. It found this encrypted
protection check and changed the value
of the encrypted branch so that, once
decrypted, the routine functions as an
always-succeed check, including the
side effects:

T0E,S05 FOUND FBFF PROTECTION CHECK
T0E,S05,$35: FB -> A2

Except...

                   ~

About a dozen moves into the game, you
get to shoot one of the aliens that are
chasing you. Unbeknownst to regular
players, this triggers a second
protection check. But if it fails, it
doesn't freeze the game. Instead, it
changes an in-game condition so the
other aliens always catch up to you and
re-imprison you, thus restarting the
game.

This second protection check is found
on T0E,S0D, encrypted with a different
1-byte XOR (#$EF this time). The patch
is the same, changing the "BNE +4" to
"BNE +5D". Now, after shooting the
alien, the game allows the player to
continue as expected.

That's the second patch that Passport
applied:

T0E,S0D FOUND FBFF PROTECTION CHECK
T0E,S0D,$95: EB -> B2

Whew. Sneaky delayed protection check!
But now we're done, right?

Welllllll...

                   ~

Much later in the game, you get to
enter a "gravtube" and push a button.
This acts as a teleporter, moving
between non-contiguous areas of the
game map.

Except when you push either button,
nothing happens. You exit the gravtube
and you're still in the same map area.

It turns out the game checksums the
second encrypted protection check on
Every. Single. Turn. And if it ever
fails, it changes another in-game flag
so that the gravtube doesn't work.

Deep within the game, there is this
little loop which adds a region of
memory to itself:

                 --v--

T0D,S0B
----------- DISASSEMBLY MODE ----------
0005:18             CLC
0006:B1 FC          LDA   ($FC),Y
0008:6D C2 47       ADC   $47C2
000B:8D C2 47       STA   $47C2
000E:88             DEY
000F:10 F5          BPL   $0006

                 --^--

Searching for references to the memory
location $47C2 finds two others:

                 --v--

------------- DISK SEARCH -------------

$0C/$01-$7B   $0D/$0B-$09   $0D/$0B-$0C
$0D/$0D-$11

                 --^--

T0C,S01 initializes $47C2 to 0.

T0D,S0D checks it.

                 --v--

T0D,S0D
----------- DISASSEMBLY MODE ----------
0010:EC C2 47       CPX   $47C2   <-- !
0013:F0 09          BEQ   $001E
0015:AE FD 4C       LDX   $4CFD
0018:8E 86 4F       STX   $4F86
001B:20 8C 4F       JSR   $4F8C
001E:AE DA 4A       LDX   $4ADA
0021:D0 03          BNE   $0026
0023:4C 14 4A       JMP   $4A14
0026:A2 00          LDX   #$00
0028:8E 16 4A       STX   $4A16
002B:BD DB 4A       LDA   $4ADB,X
002E:A8             TAY
002F:BD DC 4A       LDA   $4ADC,X
0032:AA             TAX
0033:D0 03          BNE   $0038
0035:4C 07 4A       JMP   $4A07

                 --^--

Keep in mind that I have no idea what
those other subroutines or locations do
or mean. But I know a checksum when I
see it. On a hunch, I changed the "BEQ"
at offset $0013 to a "BVC" (essentially
a branch-always, since the overflow bit
is almost always clear), and wouldn't
you know it, the gravtube now works
properly.

I found this same pattern -- initial
"decoy" protection check that freezes
the game, second delayed protection
check that sets a flag, and anti-tamper
check that ensure the second check is
intact -- on at least two other games
by Polarware.

So into Passport it goes, and here is
the updated (and God help me, final)
Passport log:

                 --v--

READING FROM S6,D1
T00,S00 FOUND PRODOS BOOTLOADER
T00,S0B VOLUME NAME IS RUN
USING BUILT-IN RWTS
WRITING TO S5,D2
T0E,S05 FOUND FBFF PROTECTION CHECK
T0E,S05,$35: FB -> A2
T0E,S0D FOUND FBFF PROTECTION CHECK
T0E,S0D,$95: EB -> B2
T0D,S0D FOUND ANTI-TAMPER CHECK
T0D,S0D,$13: F0 -> 50
CRACK COMPLETE. PRESS ANY KEY

                 --^--

Quod erat liberandum.

                   ~

               Changelog


2018-01-29

- interrupt bit -> overflow bit

2018-01-29

- update with anti-tamper check and
  explanation of side effects

2018-01-26

- initial release

---------------------------------------
A 4am crack                    No. 1643
------------------EOF------------------
